import os
import json
import requests
import re
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor, as_completed
from tqdm import tqdm
from ebooklib import epub
from typing import Dict, List, Optional

# 配置and常量
CONFIG_PATH = "config.json"
DEFAULT_CONFIG = {
    "default_format": "epub",
    "default_threads": 8,
    "api_timeout": 10,
    "show_colors": True,
    "save_cover": True
}
# 免责声明内容
DISCLAIMER = "本工具仅供学习交流，下载内容请在24小时内删除，法律责任与开发者无关（GitHub: wuxiang999）"

# 初始化目录
for d in ['download', 'config']:
    os.makedirs(d, exist_ok=True)

# 配置加载与保存
def load_config() -> Dict:
    if os.path.exists(CONFIG_PATH):
        with open(CONFIG_PATH, 'r', encoding='utf-8') as f:
            return {**DEFAULT_CONFIG,** json.load(f)}
    return DEFAULT_CONFIG.copy()

def save_config(config: Dict):
    with open(CONFIG_PATH, 'w', encoding='utf-8') as f:
        json.dump(config, f, ensure_ascii=False, indent=2)

config = load_config()

# 终端颜色配置（我的颜色我做主）
class Colors:
    RESET = "\033[0m"
    RED = "\033[31m"
    GREEN = "\033[32m"
    YELLOW = "\033[33m"
    BLUE = "\033[34m"
    PURPLE = "\033[35m"
    CYAN = "\033[36m"

    @staticmethod
    def wrap(text: str, color: str) -> str:
        return f"{color}{text}{Colors.RESET}" if config["show_colors"] else text

# 全局会话
session = requests.Session()
session.headers.update({
    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36"
})

book_info_cache = {}


def sanitize_filename(name: str) -> str:
    return re.sub(r'[\\/*?:"<>|]', '', name).strip()[:100]


def extract_book_id_from_url(url: str) -> Optional[str]:
    url = url.strip()
    fanqie_match = re.search(r'fanqienovel\.com/page/(\d+)', url)
    if fanqie_match:
        return fanqie_match.group(1)
    changdu_match = re.search(r'book_id=(\d+)', url)
    if changdu_match:
        return changdu_match.group(1)
    if url.isdigit() and len(url) >= 10:
        return url
    num_match = re.search(r'(\d{10,20})', url)
    if num_match:
        return num_match.group(1)
    return None


def get_book_metadata(book_id: str) -> Dict:
    if book_id in book_info_cache:
        return book_info_cache[book_id]
    
    metadata = {
        "book_name": f"未知书名_{book_id}",
        "author": "未知作者",
        "summary": "无简介",
        "cover_url": "",
        "category": "未知类型",
        "status": "未知状态",
        "word_count": "未知字数",
        "readers": "未知在读人数"
    }
    
    # 优先API获取
    api_success = False
    try:
        api_url = "https://api5-normal-sinfonlineb.fqnovel.com/reading/bookapi/multi-detail/v/"
        params = {
            "aid": "1967",
            "iid": "1",
            "version_code": "999",
            "book_id": book_id
        }
        resp = session.get(api_url, params=params, timeout=config["api_timeout"])
        resp.raise_for_status()
        data = resp.json()
        
        if data.get("code") == 0 and data.get("data"):
            book_data = data["data"][0]
            metadata["book_name"] = book_data.get("book_name", metadata["book_name"])
            metadata["author"] = book_data.get("author", metadata["author"])
            metadata["summary"] = book_data.get("abstract", metadata["summary"])
            metadata["cover_url"] = book_data.get("bookshelf_thumb_url") or book_data.get("thumb_url", "")
            metadata["category"] = book_data.get("category", metadata["category"])
            metadata["status"] = "连载中" if book_data.get("serial_status") == 1 else "已完结"
            metadata["word_count"] = f"{int(book_data.get('word_count', 0)/10000):.1f}万字" if book_data.get("word_count") else metadata["word_count"]
            metadata["readers"] = book_data.get("sub_info", metadata["readers"])
            
            if metadata["book_name"].startswith("《") and metadata["book_name"].endswith("》"):
                metadata["book_name"] = metadata["book_name"][1:-1]
            api_success = True
    except Exception as e:
        print(f"{Colors.wrap('API获取详情失败', Colors.RED)}，尝试网页解析：{str(e)}")
    
    #酱极（降级）网络解析
    if not api_success:
        sources = [f"https://fanqienovel.com/page/{book_id}", f"https://changdunovel.com/wap/share-v2.html?book_id={book_id}"]
        for url in sources:
            try:
                resp = session.get(url, timeout=config["api_timeout"])
                soup = BeautifulSoup(resp.text, "lxml")
                
                title_tag = soup.find("h1", class_=re.compile(r"book-title|title")) or soup.find("meta", property="og:title")
                if title_tag:
                    metadata["book_name"] = title_tag.get_text(strip=True) if hasattr(title_tag, 'get_text') else title_tag['content']
                    if metadata["book_name"].startswith("《") and metadata["book_name"].endswith("》"):
                        metadata["book_name"] = metadata["book_name"][1:-1]
                
                author_tag = soup.find("div", class_=re.compile(r"author|作者")) or soup.find("meta", attrs={"name": "author"})
                if author_tag:
                    author_text = author_tag.get_text(strip=True) if hasattr(author_tag, 'get_text') else author_tag['content']
                    metadata["author"] = re.sub(r"作者[:：\s]", "", author_text).strip()
                
                intro_tag = soup.find("div", class_=re.compile(r"intro|summary"))
                if intro_tag:
                    metadata["summary"] = intro_tag.get_text(strip=True)[:800]
                
                cover_tag = soup.find("meta", property="og:image") or soup.find("img", class_=re.compile(r"cover"))
                if cover_tag:
                    metadata["cover_url"] = cover_tag['content'] if hasattr(cover_tag, 'get') else cover_tag.get('src', '')
                    if metadata["cover_url"] and not metadata["cover_url"].startswith(('http://', 'https://')):
                        metadata["cover_url"] = f"https:{metadata['cover_url']}" if metadata["cover_url"].startswith('//') else f"https://changdunovel.com{metadata['cover_url']}"
                
                if metadata["book_name"] != f"未知书名_{book_id}":
                    break
            except Exception:
                continue
    
    if len(metadata["summary"]) > 500:
        metadata["summary"] = metadata["summary"][:500] + "..."
    
    book_info_cache[book_id] = metadata
    return metadata


def get_chapter_list(book_id: str) -> List[Dict]:
    url = f"https://fanqienovel.com/api/reader/directory/detail?bookId={book_id}"
    try:
        resp = session.get(url, timeout=config["api_timeout"])
        data = resp.json()
        if data.get("code") == 0:
            chapters = []
            for volume in data["data"]["chapterListWithVolume"]:
                for chapter in volume:
                    chapters.append({"title": chapter.get("title", ""), "item_id": chapter["itemId"]})
            return chapters
        return []
    except Exception as e:
        print(f"{Colors.wrap('获取章节列表失败', Colors.RED)}: {str(e)}")
        return []


def download_chapter(item_id: str) -> Optional[str]:
    url = "https://api-sinfonlinea.fanqiesdk.com/open_sdk/reader/content/v1"
    params = {"item_id": item_id, "novelsdk_aid": "638505", "sdk_type": "4"}
    try:
        resp = session.post(url, params=params, timeout=(5, 15))
        data = resp.json()
        if data.get("code") == 0 and "content" in data.get("data", {}):
            content = data["data"]["content"]
            clean_content = content.replace('</p>', '\n').replace('&quot;', '"').replace('&amp;', '&')
            return re.sub(r'<p idx="\d+"\u003e', '', clean_content).strip()
        return None
    except Exception:
        try:
            resp = session.post(url, params=params, timeout=(5, 15))
            data = resp.json()
            if data.get("code") == 0 and "content" in data.get("data", {}):
                content = data["data"]["content"]
                clean_content = content.replace('</p>', '\n').replace('&quot;', '"').replace('&amp;', '&')
                return re.sub(r'<p idx="\d+"\u003e', '', clean_content).strip()
        except:
            return None
    return None


def build_txt(metadata: Dict, chapters: List[Dict], output_path: str):
    """TXT格式：首尾添加单独的免责声明章节"""
    with open(output_path, 'w', encoding='utf-8') as f:
        # 免责声明章节（万一被追责了我就***）
        f.write("第0章：免责声明\n\n")
        f.write(DISCLAIMER + "\n\n")
        f.write("-" * 50 + "\n\n")
        
        # 书籍信息
        f.write(f"《{metadata['book_name']}》\n")
        f.write(f"作者：{metadata['author']}\n")
        f.write(f"类型：{metadata['category']} | 状态：{metadata['status']}\n\n")
        f.write(f"简介：{metadata['summary']}\n\n")
        f.write("=" * 50 + "\n\n")
        
        # 正文章节
        for i, chap in enumerate(chapters):
            f.write(f"第{i+1}章：{chap['title']}\n\n")
            f.write(chap['content'] + "\n\n")
            f.write("-" * 50 + "\n\n")
        
        # 结尾的免责声明章节
        f.write(f"第{len(chapters)+1}章：再次声明\n\n")
        f.write(DISCLAIMER + "\n\n")
        f.write("=" * 50 + "\n")
    
    print(f"{Colors.wrap('TXT生成成功', Colors.GREEN)}：{output_path}")


def build_epub(metadata: Dict, chapters: List[Dict], output_path: str):
    """EPUB格式：首尾添加单独的免责声明章节"""
    book = epub.EpubBook()
    book.set_title(metadata["book_name"])
    book.add_author(metadata["author"])
    book.set_language("zh-CN")
    book.add_metadata('DC', 'description', metadata["summary"])
    
    # 添加封面
    if config["save_cover"] and metadata["cover_url"]:
        try:
            resp = session.get(metadata["cover_url"], timeout=10)
            if resp.status_code == 200:
                book.set_cover("cover.jpg", resp.content)
        except Exception as e:
            print(f"{Colors.wrap('封面添加失败', Colors.YELLOW)}: {str(e)}")
    
    # 章节列表（包含首尾的免责声明）
    epub_chapters = []
    spine = ["nav"]
    
    # 开头的免责声明章节
    disclaim_start = epub.EpubHtml(title="免责声明", file_name="disclaim_start.xhtml", lang="zh-CN")
    disclaim_start.content = f"<h1>免责声明</h1><p>{DISCLAIMER}</p>"
    book.add_item(disclaim_start)
    epub_chapters.append(disclaim_start)
    spine.append(disclaim_start)
    
    # 书籍信息章节
    intro = epub.EpubHtml(title="书籍信息", file_name="intro.xhtml", lang="zh-CN")
    intro.content = f"""
    <h1>《{metadata['book_name']}》</h1>
    <p><strong>作者：</strong>{metadata['author']}</p>
    <p><strong>类型：</strong>{metadata['category']} | <strong>状态：</strong>{metadata['status']}</p>
    <p><strong>简介：</strong>{metadata['summary'].replace('\n', '<br/>')}</p>
    """
    book.add_item(intro)
    epub_chapters.append(intro)
    spine.append(intro)
    
    # 正文章节
    for i, chap in enumerate(chapters):
        if not chap["content"]:
            continue
        c = epub.EpubHtml(
            title=f"第{i+1}章：{chap['title']}",
            file_name=f"chap_{i+1}.xhtml",
            lang="zh-CN"
        )
        c.content = f"<h2>第{i+1}章：{chap['title']}</h2><p>{chap['content'].replace('\n', '<br/>')}</p>"
        book.add_item(c)
        epub_chapters.append(c)
        spine.append(c)
    
    # 结尾的免责声明章节
    disclaim_end = epub.EpubHtml(title="再次声明", file_name="disclaim_end.xhtml", lang="zh-CN")
    disclaim_end.content = f"<h1>再次声明</h1><p>{DISCLAIMER}</p>"
    book.add_item(disclaim_end)
    epub_chapters.append(disclaim_end)
    spine.append(disclaim_end)
    
    # 目录与导航
    book.toc = tuple(epub_chapters)
    book.add_item(epub.EpubNcx())
    book.add_item(epub.EpubNav())
    book.spine = spine
    
    epub.write_epub(output_path, book, {})
    print(f"{Colors.wrap('EPUB生成成功', Colors.GREEN)}：{output_path}")


def download_novel(book_id: str):
    try:
        print(f"\n{Colors.wrap('===== 书籍信息 =====', Colors.BLUE)}")
        metadata = get_book_metadata(book_id)
        print(f"书名：{Colors.wrap(metadata['book_name'], Colors.PURPLE)}")
        print(f"作者：{metadata['author']}")
        print(f"类型：{metadata['category']} | 状态：{metadata['status']}")
        
        confirm = input(f"\n{Colors.wrap('确认下载以上书籍？(y/n): ', Colors.CYAN)}").strip().lower()
        if confirm != 'y':
            print(f"{Colors.wrap('已取消下载', Colors.YELLOW)}")
            return
        
        chapters = get_chapter_list(book_id)
        if not chapters:
            print(f"{Colors.wrap('未找到章节列表，下载失败', Colors.RED)}")
            return
        total_chapters = len(chapters)
        print(f"\n{Colors.wrap(f'发现 {total_chapters} 个章节，开始下载...', Colors.GREEN)}")
        
        chap_contents = []
        with ThreadPoolExecutor(max_workers=config["default_threads"]) as executor:
            futures = {executor.submit(download_chapter, chap["item_id"]): i for i, chap in enumerate(chapters)}
            for future in tqdm(as_completed(futures), total=total_chapters, desc="下载进度"):
                idx = futures[future]
                try:
                    content = future.result()
                    chap_contents.append({"title": chapters[idx]["title"], "content": content or f"【章节 {idx+1} 下载失败】"})
                except Exception as e:
                    chap_contents.append({"title": chapters[idx]["title"], "content": f"【章节 {idx+1} 下载失败：{str(e)}】"})
        
        chap_contents.sort(key=lambda x: chapters.index(next(c for c in chapters if c["title"] == x["title"])))
        
        fname = f"{sanitize_filename(metadata['book_name'])}-{sanitize_filename(metadata['author'])}"
        if '未知' in fname:
            fname += f"_{book_id}"
        output_path = os.path.join('download', f"{fname}.{config['default_format']}")
        
        if config["default_format"] == "epub":
            build_epub(metadata, chap_contents, output_path)
        else:
            build_txt(metadata, chap_contents, output_path)
        
        print(f"\n{Colors.wrap('下载完成！', Colors.GREEN)}文件保存至：{os.path.abspath(output_path)}")
        
    except Exception as e:
        print(f"{Colors.wrap('下载失败', Colors.RED)}: {str(e)}")


def show_downloaded_books():
    print(f"\n{Colors.wrap('===== 已下载书籍 =====', Colors.BLUE)}")
    books = [f for f in os.listdir('download') if f.endswith(('.txt', '.epub'))]
    
    if not books:
        print(f"{Colors.wrap('暂无已下载书籍', Colors.YELLOW)}")
        return
    
    for i, book in enumerate(books, 1):
        size = os.path.getsize(os.path.join('download', book))
        size_str = f"{size/1024:.1f}KB" if size < 1024*1024 else f"{size/1024/1024:.1f}MB"
        fmt = "EPUB" if book.endswith('.epub') else "TXT"
        print(f"{i}. {Colors.wrap(book, Colors.CYAN)} | 格式：{fmt} | 大小：{size_str}")
    
    try:
        choice = input(f"\n{Colors.wrap('输入序号操作（0返回）: ', Colors.CYAN)}").strip()
        if choice == '0':
            return
        idx = int(choice) - 1
        if 0 <= idx < len(books):
            book_path = os.path.join('download', books[idx])
            action = input(f"{Colors.wrap('1. 打开文件 2. 删除文件: ', Colors.CYAN)}").strip()
            if action == '1':
                if os.name == 'nt':
                    os.startfile(book_path)
                else:
                    os.system(f'open "{book_path}"' if os.name == 'posix' else f'xdg-open "{book_path}"')
            elif action == '2':
                if input(f"{Colors.wrap(f'确认删除？(y/n): ', Colors.YELLOW)}").strip().lower() == 'y':
                    os.remove(book_path)
                    print(f"{Colors.wrap('删除成功', Colors.GREEN)}")
    except (ValueError, IndexError):
        print(f"{Colors.wrap('输入无效', Colors.RED)}")


def settings_menu():
    global config
    while True:
        print(f"\n{Colors.wrap('===== 系统设置 =====', Colors.PURPLE)}")
        print(f"1. 默认下载格式：{Colors.wrap(config['default_format'].upper(), Colors.CYAN)}")
        print(f"2. 默认线程数：{Colors.wrap(config['default_threads'], Colors.CYAN)}")
        print(f"3. 返回主菜单")
        
        choice = input(f"{Colors.wrap('请选择设置项 [1-3]: ', Colors.CYAN)}").strip()
        if choice == '3':
            save_config(config)
            print(f"{Colors.wrap('设置已保存', Colors.GREEN)}")
            break
        elif choice == '1':
            fmt = input(f"{Colors.wrap('请输入默认格式 (epub/txt): ', Colors.CYAN)}").strip().lower()
            if fmt in ['epub', 'txt']:
                config["default_format"] = fmt
                print(f"{Colors.wrap(f'默认格式已设为 {fmt}', Colors.GREEN)}")
            else:
                print(f"{Colors.wrap('格式无效', Colors.RED)}")
        elif choice == '2':
            try:
                threads = int(input(f"{Colors.wrap('请输入默认线程数 (1-16): ', Colors.CYAN)}").strip())
                if 1 <= threads <= 16:
                    config["default_threads"] = threads
                    print(f"{Colors.wrap(f'默认线程数已设为 {threads}', Colors.GREEN)}")
                else:
                    print(f"{Colors.wrap('线程数必须在1-16之间', Colors.RED)}")
            except ValueError:
                print(f"{Colors.wrap('输入必须为数字', Colors.RED)}")
        else:
            print(f"{Colors.wrap('无效选项', Colors.RED)}")


def main():
    print(f"{Colors.wrap('='*50, Colors.BLUE)}")
    print(f"{Colors.wrap('          番茄小说下载器          ', Colors.PURPLE)}")
    print(f"{Colors.wrap('='*50, Colors.BLUE)}")
    
    while True:
        print(f"\n{Colors.wrap('【主菜单】', Colors.BLUE)}")
        print(f"1. {Colors.wrap('下载新书籍', Colors.GREEN)}")
        print(f"2. {Colors.wrap('查看已下载书籍', Colors.GREEN)}")
        print(f"3. {Colors.wrap('设置', Colors.GREEN)}")
        print(f"4. {Colors.wrap('退出', Colors.RED)}")
        
        choice = input(f"{Colors.wrap('请选择功能 [1-4]: ', Colors.CYAN)}").strip()
        if choice == '4':
            print(f"{Colors.wrap('感谢使用，再见！', Colors.PURPLE)}")
            break
        elif choice == '2':
            show_downloaded_books()
        elif choice == '3':
            settings_menu()
        elif choice == '1':
            user_input = input(f"{Colors.wrap('请输入书籍ID或链接: ', Colors.CYAN)}").strip()
            book_id = extract_book_id_from_url(user_input)
            if not book_id:
                print(f"{Colors.wrap('无法解析书籍ID，请检查输入', Colors.RED)}")
                continue
            download_novel(book_id)
        else:
            print(f"{Colors.wrap('输入无效，请重新选择', Colors.RED)}")


if __name__ == "__main__":
    main()
